/*
 * CSSPlugin
 * Visit http://createjs.com/ for documentation, updates and examples.
 *
 * Copyright (c) 2010 gskinner.com, inc.
 *
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 *
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

/**
 * @module TweenJS
 */

this.createjs = this.createjs || {};

(function () {
  "use strict";

  /**
   * A TweenJS plugin for working with numeric CSS string properties (ex. top, left). To use simply install after
   * TweenJS has loaded:
   *
   * 	createjs.CSSPlugin.install();
   *
   * CSSPlugin works with almost any style property or unit. It identifies CSS values by looking for an initial value
   * on the element's `style` object. It also uses this initial value to parse out the units to use with that value.
   *
   * In the following example, `top` would be tweened as a style using `em` units using CSSPlugin, but `width`
   * would be not be tweened as a style (because there is no initial inline style value for it).
   *
   * 	myEl.style.top = "10em";
   * 	createjs.Tween.get(myEl).to({top:20, width:100}, 1000);
   *
   * CSSPlugin can also use computed styles. Please see {{#crossLink "AbstractTween/compute:property"}}{{/crossLink}}
   * for more information.
   *
   * CSSPlugin has specific handling for the `transform` style, and will tween any transforms as long as their operations
   * and units match. For example:
   *
   * 	myEl.style.transform = "translate(20px, 30px)";
   * 	createjs.Tween.get(myEl)
   * 		.to({transform: "translate(40px, 50px)"}, 900) // would be tweened, everything matches
   * 		.to({transform: "translate(5em, 300px)"}, 900) // would NOT be tweened, different units (px vs em)
   * 		.to({transform: "scaleX(2)"}, 900) // would NOT be tweened, different operations (translate vs rotate)
   *
   * You can also use `*` to copy the operation at that position from the previous transform.
   *
   * 	myEl.style.transform = "translate(0px, 0px) rotate(0deg)";
   * 	createjs.Tween.get(myEl)
   * 		.to({transform: "translate(50px, 50px) *"}, 900) // would copy the "rotate" operation
   * 		.to({transform: "* rotate(90deg)"}, 900) // would copy the "translate" operation
   *
   * Please note that the CSS Plugin is not included in the TweenJS minified file.
   * @class CSSPlugin
   * @constructor
   **/
  function CSSPlugin() {
    throw "CSSPlugin cannot be instantiated.";
  }
  var s = CSSPlugin;

  // static properties
  /**
   * @property priority
   * @protected
   * @static
   **/
  s.priority = 100; // high priority, should read first and write last

  /**
   * READ-ONLY. A unique identifying string for this plugin. Used by TweenJS to ensure duplicate plugins are not installed on a tween.
   * @property ID
   * @type {String}
   * @static
   * @readonly
   **/
  s.ID = "CSS";

  /**
   * READ-ONLY.
   * @property VALUE_RE
   * @type {RegExp}
   * @static
   * @readonly
   */
  s.VALUE_RE = /^(-?[\d.]+)([a-z%]*)$/; // extracts the numeric value and suffix from a single CSS value

  s.TRANSFORM_VALUE_RE = /(?:^| |,)(-?[\d.]+)([a-z%]*)/g; // extracts the numeric value and suffix from comma delimited lists

  s.TRANSFORM_RE = /(\w+?)\(([^)]+)\)|(?:^| )(\*)(?:$| )/g; // extracts the components of a transform

  /**
   * By default, CSSPlugin uses only inline styles on the target element (ie. set via the style attribute, `style` property, or `cssText`)
   * to determine which properties should be tweened via CSS, and what units to use.
   *
   * Setting `compute` to `true` causes CSSPlugin to use `getComputedStyle` for this purpose. This has the advantage of
   * including all styles that effect the target element, however there are some important considerations for its use:<UL>
   * 	<LI> `getComputedStyle` is computationally expensive, which could lead to performance issues if you are creating a large
   * 	number of tweens at once.
   * 	<LI> styles are normalized. For example, a width value specified as a `%` may be computed as `px`, which CSSPlugin will
   * 	use for the tween. Different browsers _may_ normalize values differently.
   * 	<LI> there are a large number of computed styles, which increases the chance that a property will be identified as a style.
   * 	<LI> does not work with IE8 or below.
   * 	</UL>
   *
   * 	The `compute` setting can be overridden on a per-tween basis by setting `tween.pluginData.CSS_compute`. For example,
   * 	to enable computed styles for a new tween, you could use:
   *
   * 		createjs.Tween.get(el, {pluginData:{CSS_compute:true}}).to({top:20}, 1000);
   *
   * 	Given the considerations for `compute`, it is recommended that you keep the default global setting of `false` and override it
   * 	in specific cases via `pluginData`.
   * @property compute
   * @type {Boolean}
   * @default false
   * @static
   */
  s.compute = false;

  // static methods
  /**
   * Installs this plugin for use with TweenJS. Call this once after TweenJS is loaded to enable this plugin.
   * @method install
   * @static
   **/
  s.install = function () {
    createjs.Tween._installPlugin(CSSPlugin);
  };

  /**
   * Called by TweenJS when a new property initializes on a tween.
   * See {{#crossLink "SamplePlugin/init"}}{{/crossLink}} for more info.
   * @method init
   * @param {Tween} tween
   * @param {String} prop
   * @param {any} value
   * @return {any}
   * @static
   **/
  s.init = function (tween, prop, value) {
    var data = tween.pluginData;
    if (data.CSS_disabled || !(tween.target instanceof HTMLElement)) {
      return;
    }
    var initVal = value || getStyle(tween.target, prop, data.CSS_compute);
    if (initVal === undefined) {
      return;
    }

    tween._addPlugin(CSSPlugin);
    var cssData = data.CSS || (data.CSS = {});
    if (prop === "transform") {
      cssData[prop] = "_t";
      return parseTransform(initVal);
    }

    var result = s.VALUE_RE.exec(initVal);
    if (result === null) {
      // a string we can't handle numerically, so add it to the CSSData without a suffix.
      cssData[prop] = "";
      return initVal;
    } else {
      cssData[prop] = result[2];
      return parseFloat(result[1]);
    }
  };

  /**
   * Called when a new step is added to a tween (ie. a new "to" action is added to a tween).
   * See {{#crossLink "SamplePlugin/step"}}{{/crossLink}} for more info.
   * @method step
   * @param {Tween} tween
   * @param {TweenStep} step
   * @param {Object} props
   * @static
   **/
  s.step = function (tween, step, props) {
    if (props.transform) {
      step.props.transform = parseTransform(
        step.props.transform,
        step.prev.props.transform
      );
    }
  };

  /**
   * Called before a property is updated by the tween.
   * See {{#crossLink "SamplePlugin/change"}}{{/crossLink}} for more info.
   * @method change
   * @param {Tween} tween
   * @param {TweenStep} step
   * @param {String} prop
   * @param {any} value
   * @param {Number} ratio
   * @param {Boolean} end
   * @return {any}
   * @static
   **/
  s.change = function (tween, step, prop, value, ratio, end) {
    var sfx = tween.pluginData.CSS[prop];
    if (sfx === undefined) {
      return;
    }
    if (prop === "transform") {
      value = writeTransform(step.prev.props[prop], step.props[prop], ratio);
    } else {
      value += sfx;
    }
    tween.target.style[prop] = value;
    return createjs.Tween.IGNORE;
  };

  // private helper methods:
  function getStyle(target, prop, compute) {
    if (compute || (compute == null && s.compute)) {
      return window.getComputedStyle(target)[prop];
    } else {
      return target.style[prop];
    }
  }

  function parseTransform(str, compare) {
    var result,
      list = [false, str];
    do {
      // pull out the next "component" of the transform (ex. "translate(10px, 20px)")
      result = s.TRANSFORM_RE.exec(str);
      if (!result) {
        break;
      }
      if (result[3] === "*") {
        // reuse previous value:
        list.push(compare[list.length]);
        continue;
      }
      var component = [result[1]],
        compareComp = compare && compare[list.length];

      // check that the operation type matches (ex. "translate" vs "rotate"):
      if (compare && (!compareComp || component[0] !== compareComp[0])) {
        console.log(
          "transforms don't match: ",
          component[0],
          compareComp && compareComp[0]
        );
        compare = null;
      } // component doesn't match

      parseMulti(result[2], compareComp, component);

      list.push(component);
    } while (true);

    list[0] = !!compare;
    return list;
  }

  // this was separated so that it can be used for other multi element styles in the future
  // ex. transform-origin, border, etc.
  function parseMulti(str, compare, arr) {
    // TODO: add logic to deal with "0" values? Troublesome because the browser automatically appends a unit for some 0 values.
    do {
      // pull out the next value (ex. "20px", "12.4rad"):
      var result = s.TRANSFORM_VALUE_RE.exec(str);
      if (!result) {
        return arr;
      }
      if (!arr) {
        arr = [];
      }
      arr.push(+result[1], result[2]);

      // check that the units match (ex. "px" vs "em"):
      if (compare && compare[arr.length - 1] !== result[2]) {
        console.log(
          "transform units don't match: ",
          arr[0],
          compare[arr.length - 1],
          result[2]
        );
        compare = null;
      } // unit doesn't match
    } while (true);
  }

  function writeTransform(list0, list1, ratio) {
    // check if we should just use the original transform strings:
    if (ratio === 1) {
      return list1[1];
    }
    if (ratio === 0 || !list1[0]) {
      return list0[1];
    }

    // they match, we want to apply the ratio:
    var str = "",
      l = list0.length,
      i,
      j,
      jl;
    for (i = 2; i < l; i++) {
      var component0 = list0[i],
        component1 = list1[i];
      str += component0[0] + "(";
      for (j = 1, jl = component0.length; j < jl; j += 2) {
        str += component0[j] + (component1[j] - component0[j]) * ratio; // value
        str += component1[j + 1] || component0[j + 1]; // unit
        if (j < jl - 2) {
          str += ", ";
        }
      }
      str += ")";
      if (i < l - 1) {
        str += " ";
      }
    }
    return str;
  }

  /*
	
	// this was part of an attempt to handle multi element css values, ex. margin="10px 10px 20px 30px"
	// discarded because the browser likes to collapse values, which makes a generic solution infeasible.
	// for example, margin="10px 10px 10px 10px" will collapse to just "10px"
	// requires custom logic to handle each scenario.
	s.MULTI_RE = /((?:^| )-?[\d.]+[a-z%]*){2,}/; // matches CSS values that consist of two or more values with suffixes
	function writeMulti(arr0, arr1, ratio) {
		var str = "", l=arr0.length, i;
		for (i=0; i<l; i+=2) {
			str += arr0[i]+(arr1[i]-arr0[i])*ratio+arr0[i+1];
			if (i < l-2) { str += " "; }
		}
		return str;
	}
	
	// this method is really only needed for roundtrip tests.
	function writeSingleTransform(list) {
		var str = "", l=list.length, i, j, jl, component;
		for (i=2; i<l; i++) {
			component = list[i];
			str += component[0]+"(";
			for (j=1, jl=component.length; j<jl; j+=2) {
				str += component[j]+component[j+1];
				if (j < jl-2) { str += ", "; }
			}
			str += ")";
			if (i < l-1) { str += " "; }
		}
		return str;
	}
	*/

  createjs.CSSPlugin = s;
})();
